/*
 * @(#)AutoCompletion.java 6/22/2005
 *
 * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
 */
package com.element.ui.combobox;

import com.element.swing.DelegateAction;
import com.element.swing.search.ComboBoxSearchable;
import com.element.swing.search.Searchable;
import com.element.util.AudioUtil;
import com.element.util.CompareUtil;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.tree.TreePath;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeListener;
import java.util.List;

/**
 * <code>AutoCompletion</code> is a helper class to make JTextComponent or JComboBox auto-complete based on a list of
 * known items.
 * <p/>
 * There are three constructors. The simplest one is {@link #AutoCompletion(JComboBox)}. It takes any
 * combobox and make it auto completion. If you are looking for an auto-complete combobox solution, this is all you
 * need. However <code>AutoCompletion</code> can do more than that. There are two more constructors. One is {@link
 * #AutoCompletion(JTextComponent, Searchable)}. It will use {@link Searchable} which is another
 * component available in JIDE to make the JTextCompoent auto-complete. We used Searchable here because it provides a
 * common interface to access the element in JTree, JList or JTable. In the other word, the known list item we used to
 * auto-complete can be got from JTree or JList or even JTable or any other component as long as it has Searchable
 * interface implemented. The last constructor takes any java.util.List and use it as auto completion list.
 * <p/>
 * The only option available on <code>AutoCompletion</code> is {@link #setStrict(boolean)}. If it's true, it will not
 * allow user to type in anything that is not in the known item list. If false, user can type in whatever he/she wants.
 * If the text can match with a item in the known item list, it will still auto-complete.
 * <p/>
 *
 * @author Thomas Bierhance
 * @author JIDE Software, Inc.
 */
public class AutoCompletion {

	private final Searchable _searchable;
	private JTextComponent _textComponent;

	private AutoCompletionDocument _document;

	// flag to indicate if setSelectedItem has been called
	// subsequent calls to remove/insertString should be ignored
	private boolean _selecting = false;

	private boolean _hidePopupOnFocusLoss;
	// we use this flag to workaround the bug that setText() will trigger auto-completion
	private boolean _keyTyped = false;
	private boolean _hitBackspace = false;
	private boolean _hitBackspaceOnSelection;

	private KeyListener _editorKeyListener;
//    private FocusListener _editorFocusListener;

	/** 默认不是严格的 */
	private boolean _strict = false;
	private boolean _strictCompletion = true;
	private PropertyChangeListener _propertyChangeListener;
	private JComboBox<?> _comboBox;
	private Document _oldDocument;

	/**
	 * The client property for AutoCompletion instance. When AutoCompletion is installed on a text component, this
	 * client property has the AutoCompletion.
	 */
	public static final String CLIENT_PROPERTY_AUTO_COMPLETION = "AutoCompletion";

	public AutoCompletion(final JComboBox<?> comboBox) {
		this(comboBox, new ComboBoxSearchable<>(comboBox));
	}

	public AutoCompletion(final JComboBox<?> comboBox, Searchable searchable) {
		_searchable = searchable;
		_propertyChangeListener = e -> {
			if ("editor".equals(e.getPropertyName())) {
				if (e.getNewValue() != null) {
					_textComponent = (JTextComponent) ((ComboBoxEditor) e.getNewValue()).getEditorComponent();
					configureEditor(getTextComponent());
				}
			}
		};
		_comboBox = comboBox;
		_searchable.setWildcardEnabled(false);
		if (_searchable instanceof ComboBoxSearchable) {
			((ComboBoxSearchable<?>) _searchable).setShowPopupDuringSearching(false);
		}
		_textComponent = (JTextComponent) comboBox.getEditor().getEditorComponent();
		installListeners();
	}

	public AutoCompletion(final JTextComponent textComponent, final Searchable searchable) {
		_searchable = searchable;
		_searchable.setWildcardEnabled(false);
		_textComponent = textComponent;
		registerSelectionListener(getSearchable());

		installListeners();
	}

	public AutoCompletion(final JTextComponent textComponent, final List<?> list) {
		this(textComponent, new Searchable(new JLabel()) {
			int _selectIndex = -1;

			@Override
			public int getSelectedIndex() {
				return _selectIndex;
			}

			@Override
			public void setSelectedIndex(int index, boolean incremental) {
				_selectIndex = index;
			}

			@Override
			public int getElementCount() {
				return list.size();
			}

			@Override
			public Object getElementAt(int index) {
				return list.get(index);
			}

			@Override
			public String convertElementToString(Object element) {
				return "" + element;
			}
		});
	}

	public AutoCompletion(final JTextComponent textComponent, final Object[] array) {
		this(textComponent, new Searchable(new JLabel()) {
			int _selectIndex = -1;

			@Override
			public int getSelectedIndex() {
				return _selectIndex;
			}

			@Override
			public void setSelectedIndex(int index, boolean incremental) {
				_selectIndex = index;
			}

			@Override
			public int getElementCount() {
				return array.length;
			}

			@Override
			public Object getElementAt(int index) {
				return array[index];
			}

			@Override
			public String convertElementToString(Object element) {
				return "" + element;
			}
		});
	}

	private void registerSelectionListener(Searchable searchable) {
		if (searchable.getComponent() instanceof JList) {
			final JList<?> list = (JList<?>) getSearchable().getComponent();
			list.getSelectionModel().addListSelectionListener(e -> {
				int index = list.getSelectedIndex();
				if (index != -1) {
					getTextComponent().setText(getSearchable().convertElementToString(list.getModel().getElementAt(index)));
					highlightCompletedText(0);
				}
			});
			DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, list, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0));
			DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, list, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0));
			DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, list, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0));
			DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, list, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0));
		} else if (searchable.getComponent() instanceof JTree) {
			final JTree tree = (JTree) getSearchable().getComponent();
			tree.getSelectionModel().addTreeSelectionListener(e -> {
				TreePath treePath = tree.getSelectionPath();
				if (treePath != null) {
					getTextComponent().setText("" + treePath.getLastPathComponent());
					highlightCompletedText(0);
				} else {
					getTextComponent().setText("");
				}
			});
			DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, tree, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0));
			DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, tree, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0));
			DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, tree, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0));
			DelegateAction.replaceAction(getTextComponent(), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, tree, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0));
		}
	}

	private boolean isKeyTyped() {
		return _keyTyped;
	}

	private void setKeyTyped(boolean keyTyped) {
		_keyTyped = keyTyped;
	}

	private void setInitValue() {
		int index = getSearchable().getSelectedIndex();
		if (index != -1) {
			Object selected = getSearchable().getElementAt(index);
			if (selected != null)
				_document.setText(getSearchable().convertElementToString(selected));
			highlightCompletedText(0);
		} else {
			_document.setText("");
		}
	}

	/**
	 * Uninstalls the listeners so that the component is not auto-completion anymore.
	 */
	public void uninstallListeners() {
		if (_propertyChangeListener != null && _comboBox != null) {
			_comboBox.removePropertyChangeListener(_propertyChangeListener);
		}

		if (getTextComponent() != null) {
			getTextComponent().removeKeyListener(_editorKeyListener);
//            getTextComponent().removeFocusListener(_editorFocusListener);
			String text = getTextComponent().getText();
			if (_oldDocument != null) {
				getTextComponent().setDocument(_oldDocument);
				_oldDocument = null;
			}
			getTextComponent().setText(text);
		}
		getTextComponent().putClientProperty(CLIENT_PROPERTY_AUTO_COMPLETION, null);
	}

	/**
	 * Installs the listeners needed for auto-completion feature. Please note, this method is already called when you
	 * create AutoCompletion. Unless you called {@link #uninstallListeners()}, there is no need to call this method
	 * yourself.
	 */
	public void installListeners() {
		if (_comboBox != null && _propertyChangeListener != null) {
			_comboBox.addPropertyChangeListener(_propertyChangeListener);
		}

		_editorKeyListener = new KeyAdapter() {
			private boolean _deletePressed;
			private String _saveText;

			@Override
			public void keyPressed(KeyEvent e) {
				_hitBackspace = false;

				if (KeyEvent.VK_ESCAPE != e.getKeyCode()) {
					setKeyTyped(true);
				}

				switch (e.getKeyCode()) {
					// determine if the pressed key is backspace (needed by the remove method)
					case KeyEvent.VK_BACK_SPACE:
						if (isStrict()) {
							_hitBackspace = true;
							_hitBackspaceOnSelection = getTextComponent().getSelectionStart() != getTextComponent().getSelectionEnd();
						}
						break;
					// ignore delete key
					case KeyEvent.VK_DELETE:
						if (isStrict()) {
							_deletePressed = true;
							_saveText = getTextComponent().getText();
						}
						break;
				}
			}

			@Override
			public void keyReleased(KeyEvent e) {
				super.keyReleased(e);

				try {
					if (_deletePressed) {
						_deletePressed = false;
						String text = getTextComponent().getText();
						int index = getSearchable().findFirst(text);
						if (index != -1) {
							if (text.length() == 0) {
								setSelectedItem("");
							} else {
								Object item = getSearchable().getElementAt(index);
								setSelectedItem(item);
								getTextComponent().setText(getSearchable().convertElementToString(item)); // this is what auto complete is
								// select the completed part
								highlightCompletedText(text.length());
							}
						} else { // didn't find a matching one
							if (isStrict()) {
								getTextComponent().setText(_saveText);
								e.consume();
								AudioUtil.notifyUser(_textComponent);
							}
						}
					}
				} finally {
					if (KeyEvent.VK_ESCAPE != e.getKeyCode()) {
						setKeyTyped(false);
					}
				}
			}
		};
		// Bug 5100422 on Java 1.5: Editable JComboBox won't hide popup when tabbing out
		_hidePopupOnFocusLoss = true;
//        // Highlight whole text when gaining focus
//        _editorFocusListener = new FocusAdapter() {
//            @Override
//            public void focusGained(FocusEvent e) {
//                highlightCompletedText(0);
//            }
//
//            @Override
//            public void focusLost(FocusEvent e) {
//                // Workaround for Bug 5100422 - Hide Popup on focus loss
////                if (_hidePopupOnFocusLoss) comboBox.setPopupVisible(false);
//            }
//        };

		_document = createDocument();
		configureEditor(getTextComponent());
		getTextComponent().putClientProperty(CLIENT_PROPERTY_AUTO_COMPLETION, this);
	}

	/**
	 * Creates AutoCompletionDocument.
	 *
	 * @return the AutoCompletionDocument.
	 */
	protected AutoCompletionDocument createDocument() {
		return new AutoCompletionDocument();
	}

	private void configureEditor(JTextComponent textComponent) {
		if (getTextComponent() != null) {
			getTextComponent().removeKeyListener(_editorKeyListener);
//            getTextComponent().removeFocusListener(_editorFocusListener);
		}

		if (textComponent != null) {
			_textComponent = textComponent;
			getTextComponent().addKeyListener(_editorKeyListener);
//            getTextComponent().addFocusListener(_editorFocusListener);
			String text = getTextComponent().getText();
			_oldDocument = getTextComponent().getDocument();
			if (_oldDocument instanceof AbstractDocument && _document != null) {
				_document.setDocumentFilter(((AbstractDocument) _oldDocument).getDocumentFilter());
			}
			getTextComponent().setDocument(_document);
			getTextComponent().setText(text);
		}
	}

	/**
	 * The document class used by <tt>AutoCompletion</tt>.
	 */
	protected class AutoCompletionDocument extends PlainDocument {
		@Override
		public void remove(int offs, int len) throws BadLocationException {
			// return immediately when _selecting an item
			if (_selecting)
				return;

			if (_hitBackspace) {
				// user hit backspace => move the selection backwards
				// old item keeps being selected
				if (offs > 0) {
					if (_hitBackspaceOnSelection) offs--;
				} else {
					// User hit backspace with the cursor positioned on the start => beep
					AudioUtil.notifyUser(_textComponent);
				}
				highlightCompletedText(offs);
			} else {
				super.remove(offs, len);
			}
		}

		@Override
		public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
			// return immediately when _selecting an item
			if (_selecting)
				return;

			// insert the string into the document
			super.insertString(offs, str, a);

			if (isKeyTyped() || isStrict()) {
				// lookup and select a matching item
				final String text = getText(0, getLength());
				int exactIndex = getSearchable().findFirstExactly(text);
				if (exactIndex != -1) {
					return;
				}
				int index = getSearchable().findFromCursor(text);
				Object item;
				if (index != -1) {
					item = getSearchable().getElementAt(index);
					setSelectedItem(item);
					setText(getSearchable().convertElementToString(item)); // this is what auto complete is
					// select the completed part
					highlightCompletedText(offs + str.length());
				} else { // didn't find a matching one
					if (isStrict()) {
						if (offs == 0 && text.equals(str)) {
							getSearchable().textChanged("");
							index = getSearchable().findFromCursor(text);
							if (index != -1) {
								item = getSearchable().getElementAt(index);
								setSelectedItem(item);
								setText(getSearchable().convertElementToString(item)); // this is what auto complete is
								// select the completed part
								highlightCompletedText(offs + str.length());
							} else { // didn't find a matching one
								if (isStrict()) {
									index = getSearchable().getSelectedIndex();
									if (index == -1) {
										if (getSearchable().getElementCount() > 0) {
											index = 0;
											getSearchable().setSelectedIndex(0, false);
										}
									}

									if (index != -1) {
										item = getSearchable().getElementAt(index);
										offs = offs - str.length();
										// imitate no insert (later on offs will be incremented by str.length(): selection won't move forward)
										AudioUtil.notifyUser(_textComponent);
										setText(getSearchable().convertElementToString(item));
										// select the completed part
										highlightCompletedText(offs + str.length());
									}
								}
							}
							return;
						}
						index = getSearchable().getSelectedIndex();
						if (index == -1) {
							if (getSearchable().getElementCount() > 0) {
								index = 0;
								getSearchable().setSelectedIndex(0, false);
							}
						}

						if (index != -1) {
							item = getSearchable().getElementAt(index);
							offs = offs - str.length();
							// imitate no insert (later on offs will be incremented by str.length(): selection won't move forward)
							AudioUtil.notifyUser(_textComponent);
							setText(getSearchable().convertElementToString(item));
							// select the completed part
							highlightCompletedText(offs + str.length());
						}
					}
				}
			}
		}

		protected void setText(String text) {
			try {
				// remove all text and insert the completed string
				if (isStrictCompletion()) {
					super.remove(0, getLength());
					super.insertString(0, text, null);
				} else {
					String existingText = super.getText(0, getLength());
					int matchIndex = Math.min(existingText.length(), text.length());
					// try to find a match
					for (int i = 0; i < existingText.length(); i++) {
						if (!existingText.substring(0, matchIndex).equalsIgnoreCase(text.substring(0, matchIndex))) {
							matchIndex--;
						}
					}
					// remove the no-match part and complete with the one in Searchable
					super.remove(matchIndex, getLength() - matchIndex);
					super.insertString(matchIndex, text.substring(matchIndex), null);
				}
			} catch (BadLocationException e) {
				throw new RuntimeException(e.toString());
			}
		}
	}

	private void highlightCompletedText(int start) {
		int length = getTextComponent().getDocument().getLength();
		getTextComponent().setCaretPosition(length);
		if (start < 0) {
			start = 0;
		}
		if (start > length) {
			start = length;
		}
		getTextComponent().moveCaretPosition(start);
	}

	private void setSelectedItem(Object item) {
		_selecting = true;
		for (int i = 0, n = getSearchable().getElementCount(); i < n; i++) {
			Object currentItem = getSearchable().getElementAt(i);
			// current item starts with the pattern?
			if (CompareUtil.equals(item, currentItem)) {
				getSearchable().setSelectedIndex(i, false);
			}
		}
		_selecting = false;
	}

	/**
	 * Gets the strict property.
	 *
	 * @return the value of strict property.
	 */
	public boolean isStrict() {
		return _strict;
	}

	/**
	 * Sets the strict property. If true, it will not allow user to type in anything that is not in the known item list.
	 * If false, user can type in whatever he/she wants. If the text can match with a item in the known item list, it
	 * will still auto-complete.
	 *
	 * @param strict
	 */
	public void setStrict(boolean strict) {
		_strict = strict;
	}

	/**
	 * Gets the strict completion property.
	 *
	 * @return the value of strict completion property.
	 * @see #setStrictCompletion(boolean)
	 */
	public boolean isStrictCompletion() {
		return _strictCompletion;
	}

	/**
	 * Sets the strict completion property. If true, in case insensitive searching, it will always use the exact item in
	 * the Searchable to replace whatever user types. For example, when Searchable has an item "Arial" and user types in
	 * "AR", if this flag is true, it will auto-completed as "Arial". If false, it will be auto-completed as "ARial". Of
	 * course, this flag will only make a difference if Searchable is case insensitive.
	 *
	 * @param strictCompletion
	 */
	public void setStrictCompletion(boolean strictCompletion) {
		_strictCompletion = strictCompletion;
	}

	/**
	 * Gets the underlying text component which auto-completes.
	 *
	 * @return the underlying text component.
	 */
	protected JTextComponent getTextComponent() {
		return _textComponent;
	}

	/**
	 * Gets the underlying Searchable. If you use the constructor {@link #AutoCompletion(JTextComponent, Searchable)},
	 * the return value will be the Searchable you passed in. If you use the other twoconstructorss, internally we will
	 * still create a Searchable. If so, this Searchable will be returned.
	 *
	 * @return the Searchable.
	 */
	public Searchable getSearchable() {
		return _searchable;
	}

	/**
	 * When auto-completion is enabled on a text component, we will set a client property on it. This method will look
	 * for this client property and return you the instance of the AutoCompletion that is installed on the component.
	 *
	 * @param component the component.
	 * @return the AutoCompletion. If null, it means there is no AutoCompletion installed.
	 */
	public static AutoCompletion getAutoCompletion(JComponent component) {
		if (component == null) return null;
		Object clientProperty = component.getClientProperty(CLIENT_PROPERTY_AUTO_COMPLETION);
		if (clientProperty instanceof AutoCompletion) {
			return (AutoCompletion) clientProperty;
		}
		return null;
	}
}
